<?php
// +----------------------------------------------------------------------+
// | Mobile user agent string parsing class for PHP5.                     |
// | Copyright (C) 2004 Craig Manley                                      |
// +----------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify |
// | it under the terms of the GNU Lesser General Public License as       |
// | published by the Free Software Foundation; either version 2.1 of the |
// | License, or (at your option) any later version.                      |
// |                                                                      |
// | This library is distributed in the hope that it will be useful, but  |
// | WITHOUT ANY WARRANTY; without even the implied warranty of           |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU     |
// | Lesser General Public License for more details.                      |
// |                                                                      |
// | You should have received a copy of the GNU Lesser General Public     |
// | License along with this library; if not, write to the Free Software  |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  |
// | USA                                                                  |
// |                                                                      |
// | LGPL license URL: http://opensource.org/licenses/lgpl-license.php    |
// +----------------------------------------------------------------------+
// | Author: Craig Manley                                                 |
// +----------------------------------------------------------------------+
//
// $Id: MobileUserAgent.class.php 118 2009-03-05 02:14:07Z sanglt $
//


/**
 * @author    Craig Manley
 * @copyright Copyright � 2004, Craig Manley. All rights reserved.
 * @version   $Revision: 1.6 $
 * @package   com.craigmanley.classes.mobile.MobileUserAgent
 */

/**
 * Parses a mobile user agent string into it's basic constituent parts, the
 * most important being vendor and model.
 *
 * One reason for doing this would be to use this information to lookup vendor-model
 * specific device characteristics in a database. Of course you could use user agent
 * profiles for this, but not all mobile phones have (valid) user agent profiles, especially
 * the older types of mobile phones.
 *
 * Another reason would be to detect if the visiting client is a mobile handset. You could do
 * it like this:
 * <pre>
 *  require_once('MobileUserAgent.php');
 *  $mua = new MobileUserAgent();
 *  $is_client_mobile = $mua->success();
 * </pre>
 *
 * Some references:
 * <ul>
 *  <li>{@link http://www.handy-ortung.com }</li>
 *  <li>{@link http://www.mobileopera.com/reference/ua }</li>
 *  <li>{@link http://www.appelsiini.net/~tuupola/php/Imode_User_Agent/source/ }</li>
 *  <li>{@link http://www.zytrax.com/tech/web/mobile_ids.html }</li>
 *  <li>{@link http://webcab.de/wapua.htm }</li>
 *  <li>{@link http://www.nttdocomo.co.jp/english/p_s/i/tag/s2.html }</li>
 *  <li>{@link http://test.waptoo.com/v2/skins/waptoo/user.asp }</li>
 * </ul>
 *
 * @package  com.craigmanley.classes.mobile.MobileUserAgent
 */
class MobileUserAgent {
	
	// Private members
	private $useragent = null;
	private $vendor = null;
	private $model = null;
	private $version = null;
	private $imode_cache = null;
	private $screendims = null;
	private $is_standard = false;
	private $is_imode = false;
	private $is_mozilla = false;
	private $is_rubbish = false;
	private $is_series60 = null;
	
	/**
	 * Constructor.
	 *
	 * @param string $useragent Optional useragent string. If null, environment variable HTTP_USER_AGENT is used.
	 */
	function __construct($useragent = null) {
		if (! (isset ( $useragent ) && strlen ( $useragent ))) {
			if (! isset ( $_SERVER ['HTTP_USER_AGENT'] )) {
				throw new Exception ( 'Environment variable HTTP_USER_AGENT is missing!' );
			}
			$useragent = $_SERVER ['HTTP_USER_AGENT'];
		}
		$this->useragent = $useragent;
		$hash = $this->_parseUserAgent ( $useragent );
		if ($hash) {
			$this->vendor = $hash ['vendor'];
			$this->model = $hash ['model'];
			if (isset ( $hash ['version'] )) {
				$this->version = $hash ['version'];
			}
			if (isset ( $hash ['imode_cache'] )) {
				$this->imode_cache = $hash ['imode_cache'];
			}
			if (isset ( $hash ['screendims'] )) {
				$this->screendims = $hash ['screendims'];
			}
		}
	}
	
	/**
	 * Parses a standard mobile user agent string with the format vendor-model/version.
	 * If no match can be made, FALSE is returned.
	 * If a match is made, an associative array is returned containing the compulsory
	 * keys "vendor" and "model", and the optional keys "version", and "screendims".
	 *
	 * Below are a few samples of these user agent strings:
	 * <pre>
	 *  Nokia8310/1.0 (05.57)
	 *  NokiaN-Gage/1.0 SymbianOS/6.1 Series60/1.2 Profile/MIDP-1.0 Configuration/CLDC-1.0
	 *  SAGEM-myX-6/1.0 UP.Browser/6.1.0.6.1.c.3 (GUI) MMP/1.0 UP.Link/1.1
	 *  SAMSUNG-SGH-A300/1.0 UP/4.1.19k
	 *  SEC-SGHE710/1.0
	 * </pre>
	 *
	 * @param string $useragent User agent string.
	 * @return mixed
	 */
	protected function _parseUserAgentStandard($useragent) {
		// Standard vendor-model/version user agents
		if (! preg_match ( '/^((ACER|Alcatel|AUDIOVOX|BlackBerry|CDM|Ericsson|LG\b|LGE|Motorola|MOT|NEC|Nokia|Panasonic|QCI|SAGEM|SAMSUNG|SEC|Sanyo|Sendo|SHARP|SIE|SonyEricsson|Telit|Telit_Mobile_Terminals|TSM)[- ]?([^\/\s\_]+))(\/(\S+))?/', $useragent, $matches )) {
			return false;
		}
		$both = $matches [1];
		$vendor = $matches [2];
		$model = $matches [3];
		$version = null;
		if (count ( $matches ) >= 6) {
			$version = $matches [5];
		}
		
		// Fixup vendors and models.
		if ($vendor == 'ACER') {
			$vendor = 'Acer';
		} elseif ($vendor == 'AUDIOVOX') {
			$vendor = 'Audiovox';
		} elseif ($vendor == 'CDM') {
			$vendor = 'Audiovox';
			$model = "CDM-$model";
		} elseif ($vendor == 'Ericsson') {
			if ($model == 'T68_NIL') {
				$model = 'T68';
			}
		} elseif (substr ( $vendor, 0, 2 ) == 'LG') {
			$vendor = 'LG';
			if (preg_match ( '/^([A-Za-z\d]+)-/', $model, $m )) { // LGE510W-V137-AU4.2
				$model = $m [1];
			}
		} elseif ($vendor == 'MOT') {
			$vendor = 'Motorola';
			$model = preg_replace ( '/[\._]$/', '', $model );
		} elseif ($vendor == 'PHILIPS') {
			$model = strtoupper ( $model );
		} elseif ($vendor == 'SAGEM') {
			if ($model == '-') {
				return false;
			}
		} elseif ($vendor == 'SEC') {
			$vendor = 'SAMSUNG';
			$model = preg_replace ( '/\*.*$/', '', $model );
		} elseif ($vendor == 'SIE') {
			$vendor = 'Siemens';
		} elseif ($vendor == 'Telit_Mobile_Terminals') {
			$vendor = 'Telit';
		} elseif ($vendor == 'TSM') {
			$vendor = 'Vitelcom';
			$model = $both;
		}
		$result = array ('vendor' => $vendor, 'model' => $model );
		if (isset ( $version )) {
			$result ['version'] = $version;
		}
		return $result;
	}
	
	/**
	 * Parses an i-mode user agent string.
	 * If no match can be made, FALSE is returned.
	 * If a match is made, an associative array is returned containing the compulsory
	 * keys "vendor" and "model", and the optional keys "version", "imode_cache",
	 * and "screendims".
	 *
	 * Below are a few samples of these user agent strings:
	 * <pre>
	 *  portalmmm/1.0 m21i-10(c10)
	 *  portalmmm/1.0 n21i-10(c10)
	 *  portalmmm/1.0 n21i-10(;ser123456789012345;icc1234567890123456789F)
	 *  portalmmm/2.0 N400i(c20;TB)
	 *  portalmmm/2.0 P341i(c10;TB)
	 *  DoCoMo/1.0/modelname
	 *  DoCoMo/1.0/modelname/cache
	 *  DoCoMo/1.0/modelname/cache/unique_id_information
	 *  DoCoMo/2.0 modelname(cache;individual_identification_information)
	 * </pre>
	 *
	 * @param string $useragent User agent string.
	 * @return mixed
	 */
	protected function _parseUserAgentImode($useragent) {
		$vendors = array ('D' => 'Mitsubishi', 'ER' => 'Ericsson', 'F' => 'Fujitsu', 'KO' => 'Kokusai', // Hitachi
'M' => 'Mitsubishi', 'P' => 'Panasonic', // Matsushita
'N' => 'NEC', 'NM' => 'Nokia', 'R' => 'Japan Radio', 'S' => 'SAMSUNG', 'SH' => 'Sharp', 'SO' => 'Sony', 'TS' => 'Toshiba' );
		// Standard i-mode user agents
		if (preg_match ( '/^(portalmmm|DoCoMo)\/(\d+\.\d+) ((' . implode ( '|', array_keys ( $vendors ) ) . ')[\w\-]+)\((c(\d+))?/i', $useragent, $matches )) {
			$result = array ('vendor' => $vendors [strtoupper ( $matches [4] )], 'model' => $matches [3], 'version' => $matches [2] );
			if ((count ( $matches ) == 7) && strlen ( $matches [6] )) {
				$result ['imode_cache'] = $matches [6] + 0;
			} else {
				$result ['imode_cache'] = 5;
			}
			return ($result);
		} 

		// DoCoMo HTML i-mode user agents
		elseif (preg_match ( '/^DoCoMo\/(\d+\.\d+)\/((' . implode ( '|', array_keys ( $vendors ) ) . ')[\w\.\-\_]+)(\/c(\d+))?/i', $useragent, $matches )) {
			// HTML 1.0: DoCoMo/1.0/modelname
			// HTML 2.0: DoCoMo/1.0/modelname/cache
			// HTML 3.0: DoCoMo/1.0/modelname/cache/unique_id_information
			$result = array ('vendor' => $vendors [strtoupper ( $matches [3] )], 'model' => $matches [2], 'version' => $matches [1] );
			if (count ( $matches ) >= 6) {
				$result ['imode_cache'] = $matches [5] + 0;
			} else {
				$result ['imode_cache'] = 5;
			}
			return $result;
		}
		
		return false;
	
	}
	
	/**
	 * Parses a Mozilla (so called) compatible user agent string.
	 * If no match can be made, FALSE is returned.
	 * If a match is made, an associative array is returned containing the compulsory
	 * keys "vendor" and "model", and the optional keys "version", and "screendims".
	 *
	 * Below are a few samples of these user agent strings:
	 * <pre>
	 *  Mozilla/4.1 (compatible; MSIE 5.0; Symbian OS; Nokia 3650;424) Opera 6.10  [en]
	 *  Mozilla/4.0 (compatible; MSIE 6.0; Nokia7650) ReqwirelessWeb/2.0.0.0
	 *  Mozilla/1.22 (compatible; MMEF20; Cellphone; Sony CMD-Z5)
	 *  Mozilla/1.22 (compatible; MMEF20; Cellphone; Sony CMD-Z5;Pz063e+wt16)
	 *  Mozilla/2.0 (compatible; MSIE 3.02; Windows CE; PPC; 240x320)
	 *  mozilla/4.0 (compatible;MSIE 4.01; Windows CE;PPC;240X320) UP.Link/5.1.1.5
	 *  Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320)
	 *  Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; SmartPhone; 176x220)
	 *  Mozilla/2.0 (compatible; MSIE 3.02; Windows CE; 240x320; PPC)
	 *  Mozilla/2.0 (compatible; MSIE 3.02; Windows CE; Smartphone; 176x220; Mio8380; Smartphone; 176x220)
	 *  Mozilla/4.0 (MobilePhone SCP-8100/US/1.0) NetFront/3.0 MMP/2.0
	 *  Mozilla/2.0(compatible; MSIE 3.02; Windows CE; Smartphone; 176x220)
	 *  Mozilla/4.1 (compatible; MSIE 5.0; Symbian OS Series 60 42) Opera 6.0 [fr]
	 *  Mozilla/SMB3(Z105)/Samsung UP.Link/5.1.1.5
	 * </pre>
	 *
	 * @param string $useragent User agent string.
	 * @return mixed
	 */
	protected function _parseUserAgentMozilla($useragent) {
		// SAMSUNG browsers
		if (preg_match ( '/^Mozilla\/SMB3\((Z105)\)\/(Samsung)/', $useragent, $matches )) {
			return (array ('vendor' => strtoupper ( $matches [2] ), 'model' => $matches [1] ));
		}
		
		// Extract the string between the brackets.
		if (! preg_match ( '/^Mozilla\/\d+\.\d+\s*\(([^\)]+)\)/i', $useragent, $matches )) {
			return false;
		}
		$parts = preg_split ( '/\s*;\s*/', $matches [1] ); // split string between brackets on ';' seperator.
		

		// Micro$oft PPC and Smartphone browsers. Unfortunately, one day, if history repeats itself, this will probably be the only user-agent check necessary.
		if ((count ( $parts ) >= 4) && ($parts [0] == 'compatible') && ($parts [2] == 'Windows CE')) {
			$result = array ('vendor' => 'Microsoft' );
			if (($parts [3] == 'PPC') || (strtolower ( $parts [3] ) == 'smartphone')) {
				$result ['model'] = 'SmartPhone';
				if ((count ( $parts ) >= 5) && preg_match ( '/^\d{1,4}x\d{1,4}$/i', $parts [4] )) {
					$result ['screendims'] = strtolower ( $parts [4] );
				}
			} elseif ((count ( $parts ) >= 5) && (($parts [4] == 'PPC') || (strtolower ( $parts [4] ) == 'smartphone'))) {
				$result ['model'] = 'SmartPhone';
				if (preg_match ( '/^\d{1,4}x\d{1,4}$/i', $parts [3] )) {
					$result ['screendims'] = strtolower ( $parts [3] );
				}
			}
			if (array_key_exists ( 'model', $result )) {
				return $result;
			}
		}
		
		// Nokia's with Opera browsers or SonyEricssons.
		if ((count ( $parts ) >= 4) && ($parts [0] == 'compatible') && preg_match ( '/^(Nokia|Sony)\s*(\S+)$/', $parts [3], $matches )) {
			if ($matches [1] == 'Sony') {
				$matches [1] = 'SonyEricsson';
			}
			return (array ('vendor' => $matches [1], 'model' => $matches [2] ));
		}
		
		// SANYO browsers
		if ((count ( $parts ) >= 1) && preg_match ( '/^MobilePhone ([^\/]+)\/([A-Z]+\/)?(\d+\.\d+)$/', $parts [0], $matches )) { // MobilePhone PM-8200/US/1.0
			return (array ('vendor' => 'Sanyo', 'model' => $matches [1], 'version' => $matches [3] ));
		}
		
		// Nokias with ReqwirelessWeb browser
		if ((count ( $parts ) >= 3) && ($parts [0] == 'compatible') && preg_match ( '/^(Nokia)\s*(\S+)$/', $parts [1], $matches )) {
			return (array ('vendor' => $matches [1], 'model' => $matches [2] ));
		}
		
		return false;
	}
	
	/**
	 * Parses a non-standard mobile user agent string.
	 * If no match can be made, FALSE is returned.
	 * If a match is made, an associative array is returned containing the compulsory
	 * keys "vendor" and "model", and the optional keys "version", and "screendims".
	 *
	 * Below are a few samples of these user agent strings:
	 * <pre>
	 *  LGE/U8150/1.0 Profile/MIDP-2.0 Configuration/CLDC-1.0
	 *  PHILIPS855 ObigoInternetBrowser/2.0
	 *  PHILIPS 535 / Obigo Internet Browser 2.0
	 *  PHILIPS-FISIO 620/3
	 *  PHILIPS-Fisio311/2.1
	 *  PHILIPS-FISIO311/2.1
	 *  PHILIPS-Xenium9@9 UP/4.1.16r
	 *  PHILIPS-XENIUM 9@9/2.1
	 *  PHILIPS-Xenium 9@9++/3.14
	 *  PHILIPS-Ozeo UP/4
	 *  PHILIPS-V21WAP UP/4
	 *  PHILIPS-Az@lis288 UP/4.1.19m
	 *  PHILIPS-SYSOL2/3.11 UP.Browser/5.0.1.11
	 *  Vitelcom-Feature Phone1.0 UP.Browser/5.0.2.2(GUI
	 *  ReqwirelessWeb/2.0.0 MIDP-1.0 CLDC-1.0 Nokia3650
	 *  SEC-SGHE710
	 * </pre>
	 * Notice how often one certain brand of these user-agents is handled by this function. I say no more.
	 *
	 * @param string $useragent User agent string.
	 * @return mixed
	 */
	protected function _parseUserAgentRubbish($useragent) {
		// Old ReqwirelessWeb browsers for Nokia. ReqwirelessWeb/2.0.0 MIDP-1.0 CLDC-1.0 Nokia3650
		if (preg_match ( '/(Nokia)\s*(N-Gage|\d+)$/', $useragent, $matches )) {
			return (array ('vendor' => $matches [1], 'model' => $matches [2] ));
		} 

		// LG Electronics
		elseif (preg_match ( '/^(LG)E?\/(\w+)(\/(\d+\.\d+))?/', $useragent, $matches )) { // LGE/U8150/1.0 Profile/MIDP-2.0 Configuration/CLDC-1.0
			$result = array ('vendor' => $matches [1], 'model' => $matches [2] );
			if ((count ( $matches ) == 5) && strlen ( $matches [4] )) {
				$result ['version'] = $matches [4];
			}
			return $result;
		} 

		// And now for the worst of all user agents, those that start with the text 'PHILIPS'.
		elseif (preg_match ( '/^(PHILIPS)(.+)/', $useragent, $matches )) {
			$vendor = $matches [1];
			$model = null;
			$garbage = trim ( strtoupper ( $matches [2] ) ); // everything after the word PHILIPS in uppercase.
			if (preg_match ( '/^-?(\d+)/', $garbage, $matches )) { // match the model names that are just digits.
				$model = $matches [1];
				// PHILIPS855 ObigoInternetBrowser/2.0
			// PHILIPS 535 / Obigo Internet Browser 2.0
			} elseif (preg_match ( '/^-?(FISIO)\s*(\d+)/', $garbage, $matches )) { // match the FISIO model names.
				$model = $matches [1] . $matches [2];
				// PHILIPS-FISIO 620/3
			// PHILIPS-Fisio311/2.1
			// PHILIPS-FISIO311/2.1
			} elseif (preg_match ( '/^-?(XENIUM)/', $garbage, $matches )) { // match the XENIUM model names.
				$model = $matches [1];
				// PHILIPS-Xenium9@9 UP/4.1.16r
			// PHILIPS-XENIUM 9@9/2.1
			// PHILIPS-Xenium 9@9++/3.14
			} elseif (preg_match ( '/^-?([^\s\/]+)/', $garbage, $matches )) { // match all other model names that contain no spaces and no slashes.
				$model = $matches [1];
				// PHILIPS-Ozeo UP/4
			// PHILIPS-V21WAP UP/4
			// PHILIPS-Az@lis288 UP/4.1.19m
			// PHILIPS-SYSOL2/3.11 UP.Browser/5.0.1.11
			}
			if (isset ( $model )) {
				return (array ('vendor' => $vendor, 'model' => $model ));
			}
		} 

		// Vitelcom user-agents (used in Spain)
		elseif (preg_match ( '/^(Vitelcom)-(Feature Phone)(\d+\.\d+)/', $useragent, $matches )) {
			// Vitelcom-Feature Phone1.0 UP.Browser/5.0.2.2(GUI)  -- this is a TSM 3 or a TSM 4.
			return (array ('vendor' => $matches [1], 'model' => $matches [2], 'version' => $matches [3] ));
		}
		
		return false;
	}
	
	/**
	 * Parses a user agent string.
	 * This method simply calls the other 4 _parseUserAgent*() methods to do the work.
	 * If no match can be made, FALSE is returned.
	 * If a match is made, an associative array is returned containing the compulsory
	 * keys "vendor" and "model", and the optional keys "version", "imode_cache",
	 * and "screendims".
	 *
	 * @param string $useragent User agent string.
	 * @return mixed
	 */
	protected function _parseUserAgent($useragent) {
		if ($result = $this->_parseUserAgentStandard ( $useragent )) {
			$this->is_standard = true;
			return $result;
		}
		if ($result = $this->_parseUserAgentMozilla ( $useragent )) {
			$this->is_mozilla = true;
			return $result;
		}
		if ($result = $this->_parseUserAgentImode ( $useragent )) {
			$this->is_imode = true;
			return $result;
		}
		if ($result = $this->_parseUserAgentRubbish ( $useragent )) {
			$this->is_rubbish = true;
			return $result;
		}
		return false;
	}
	
	/**
	 * Returns true if the user-agent string passed into the constructor could be parsed, else false.
	 * If this method returns false, then it's probably not a mobile user agent string that was
	 * passed into the constructor.
	 *
	 * @return boolean
	 */
	public function success() {
		return isset ( $this->vendor );
	}
	
	/**
	 * Returns the user agent string as passed into the constructor or read
	 * from the environment variable HTTP_USER_AGENT.
	 *
	 * @return string
	 */
	public function userAgent() {
		return $this->useragent;
	}
	
	/**
	 * Returns the vendor of the handset if success() returns true, else null.
	 *
	 * @return string|null
	 */
	public function vendor() {
		return $this->vendor;
	}
	
	/**
	 * Returns the model of the handset if success() returns true, else null.
	 *
	 * @return string|null
	 */
	public function model() {
		return $this->model;
	}
	
	/**
	 * Returns the version (if any) of the user agent.
	 * The version information isn't always present, nor reliable.
	 *
	 * @return string|null
	 */
	public function version() {
		return $this->version;
	}
	
	/**
	 * Determines if the parsed user-agent string belongs to an i-mode handset.
	 * Returns true, if so, else false.
	 *
	 * @return boolean
	 */
	public function isImode() {
		return $this->is_imode;
	}
	
	/**
	 * Determines if the parsed user-agent string has a Mozilla 'compatible' format.
	 * Returns true, if so, else false.
	 *
	 * @return boolean
	 */
	public function isMozilla() {
		return $this->is_mozilla;
	}
	
	/**
	 * Determines if the parsed user-agent string has a standard vendor-model/version format.
	 * Returns true, if so, else false.
	 *
	 * @return boolean
	 */
	public function isStandard() {
		return $this->is_standard;
	}
	
	/**
	 * Determines if the parsed user-agent string has a non-standard or messed up format.
	 * Returns true, if so, else false.
	 *
	 * @return boolean
	 */
	public function isRubbish() {
		return $this->is_rubbish;
	}
	
	/**
	 * Returns the maximum i-mode cache data size in kb's of the user agent if it is
	 * an i-mode user-agent, else null.
	 *
	 * @return integer|null
	 */
	public function imodeCache() {
		return $this->imode_cache;
	}
	
	/**
	 * Returns the screen dimensions in the format wxh if this information was parsed
	 * from the user agent string itself, else null.
	 *
	 * @return string|null
	 */
	public function screenDims() {
		return $this->screendims;
	}
	
	/**
	 * Determines if this is a Symbian OS Series 60 user-agent string.
	 *
	 * @return boolean
	 */
	public function isSeries60() {
		if (! isset ( $this->is_series60 )) {
			//  NokiaN-Gage/1.0 SymbianOS/6.1 Series60/1.2 Profile/MIDP-1.0 Configuration/CLDC-1.0
			//  Mozilla/4.1 (compatible; MSIE 5.0; Symbian OS Series 60 42) Opera 6.0 [fr]
			$this->is_series60 = preg_match ( '/\b(Symbian OS Series 60|SymbianOS\/\S+ Series60)\b/', $this->useragent );
		}
		return $this->is_series60;
	}

}

?>